-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add progressive enhancement example #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add a complete todo app example demonstrating progressive enhancement: - Works with JavaScript: WebSocket-based real-time updates - Works without JavaScript: Traditional HTTP form submissions with PRG pattern Features demonstrated: - Form submissions via both lvt-submit (JS) and method="POST" (no-JS) - Server-side validation with inline error display - Flash messages after successful actions - Auto-generated content-based keys (no explicit data-key needed) The example includes comprehensive chromedp E2E tests covering: - JavaScript mode WebSocket form submission - No-JS mode HTTP form submission with redirect - Validation error handling - Toggle and delete actions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a comprehensive progressive enhancement example demonstrating how to build applications that work both with and without JavaScript enabled. The example implements a todo list application that uses WebSocket for real-time updates when JavaScript is available, and falls back to traditional HTTP form submissions with the POST-Redirect-GET pattern when JavaScript is disabled.
Changes:
- Adds a complete todo app with dual-mode forms supporting both WebSocket and HTTP POST
- Implements PRG pattern for non-JS clients to prevent duplicate submissions
- Includes comprehensive E2E tests using chromedp for both JS and no-JS scenarios
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| progressive-enhancement/main.go | Implements TodoController with Mount, Add, Toggle, and Delete actions |
| progressive-enhancement/progressive-enhancement.tmpl | HTML template with dual-mode forms and progressive enhancement support |
| progressive-enhancement/progressive_enhancement_test.go | Comprehensive E2E tests covering JS, no-JS, validation, and content negotiation |
| progressive-enhancement/README.md | Documentation explaining progressive enhancement concepts and usage |
| go.mod | Adds local replace directive for development |
| go.sum | Updates checksums for dependencies |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
progressive-enhancement/main.go
Outdated
| for i := range state.Items { | ||
| if state.Items[i].ID == id { | ||
| state.Items = append(state.Items[:i], state.Items[i+1:]...) | ||
| ctx.SetFlash("success", "Item deleted") | ||
| break | ||
| } | ||
| } |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Delete action modifies the slice while iterating over it by using append with slice expressions. If the item to delete is not found, the function silently returns without any indication. Consider adding a flash message or error when the ID is not found, or at minimum ensure the flash message is only set when an item is actually deleted.
| for i := range state.Items { | |
| if state.Items[i].ID == id { | |
| state.Items = append(state.Items[:i], state.Items[i+1:]...) | |
| ctx.SetFlash("success", "Item deleted") | |
| break | |
| } | |
| } | |
| // Find index of the item to delete without modifying the slice during iteration. | |
| deleteIndex := -1 | |
| for i, item := range state.Items { | |
| if item.ID == id { | |
| deleteIndex = i | |
| break | |
| } | |
| } | |
| if deleteIndex >= 0 { | |
| state.Items = append(state.Items[:deleteIndex], state.Items[deleteIndex+1:]...) | |
| ctx.SetFlash("success", "Item deleted") | |
| } else { | |
| ctx.SetFlash("error", "Item not found") | |
| } |
| id := ctx.GetString("id") | ||
| for i := range state.Items { | ||
| if state.Items[i].ID == id { | ||
| state.Items[i].Completed = !state.Items[i].Completed | ||
| break | ||
| } | ||
| } |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Toggle action does not provide any feedback to the user when the toggle ID is not found. Consider adding a flash message for error cases or success confirmation, similar to the Delete action.
| id := ctx.GetString("id") | |
| for i := range state.Items { | |
| if state.Items[i].ID == id { | |
| state.Items[i].Completed = !state.Items[i].Completed | |
| break | |
| } | |
| } | |
| id := ctx.GetString("id") | |
| found := false | |
| for i := range state.Items { | |
| if state.Items[i].ID == id { | |
| state.Items[i].Completed = !state.Items[i].Completed | |
| found = true | |
| ctx.SetFlash("success", "Item updated") | |
| break | |
| } | |
| } | |
| if !found { | |
| ctx.SetFlash("error", "Item not found") | |
| } |
|
|
||
| - `main.go` - Controller, state, and action handlers | ||
| - `progressive-enhancement.tmpl` - Template with dual-mode forms | ||
| - `README.md` - This documentation |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Files section lists only three files but omits the test file. Consider adding progressive_enhancement_test.go to the list since it's part of the example and demonstrates E2E testing patterns.
| - `README.md` - This documentation | |
| - `README.md` - This documentation | |
| - `progressive_enhancement_test.go` - End-to-end tests for progressive enhancement behavior |
| ctx, cancel := chromedp.NewContext(allocCtx) | ||
| defer cancel() | ||
|
|
||
| // Set timeout | ||
| ctx, cancel = context.WithTimeout(ctx, 30*time.Second) | ||
| defer cancel() |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cancel function is being reassigned on line 101, which shadows the previous cancel function from line 97. This means the first context created on line 97 will not be properly canceled. Consider using different variable names for the timeout context and its cancel function.
| ctx, cancel = context.WithTimeout(ctx, 15*time.Second) | ||
| defer cancel() |
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cancel function is being reassigned on line 54, which shadows the previous cancel function from line 50. This means the first context created on line 50 will not be properly canceled. Consider using different variable names for the timeout context and its cancel function.
| ctx, cancel = context.WithTimeout(ctx, 15*time.Second) | |
| defer cancel() | |
| ctx, timeoutCancel := context.WithTimeout(ctx, 15*time.Second) | |
| defer timeoutCancel() |
go.mod
Outdated
| // Use local livetemplate for progressive enhancement development | ||
| replace github.com/livetemplate/livetemplate => ../../../livetemplate/.worktrees/progressive-enhancement | ||
|
|
Copilot
AI
Jan 18, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The replace directive points to a local worktree path that is specific to the development environment. This will break the build for anyone else trying to use this example. The replace directive should either be removed before merging (if the dependency is already published), or changed to point to a more portable location, or this file should be gitignored with a .example version checked in instead.
| // Use local livetemplate for progressive enhancement development | |
| replace github.com/livetemplate/livetemplate => ../../../livetemplate/.worktrees/progressive-enhancement |
Use pseudo-version pointing to the progressive-enhancement branch commit instead of local replace directive for CI compatibility. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Improvements based on Copilot review feedback: 1. **Toggle action**: Added feedback for both success and not-found cases with flash messages (Copilot suggestion) 2. **Delete action**: Fixed slice modification during iteration by finding index first, then removing. Added not-found error handling (Copilot suggestion) 3. **README**: Added test file to the Files section (Copilot suggestion) 4. **Test file**: Fixed cancel function shadowing by using distinct variable names (timeoutCancel) for timeout contexts (Copilot suggestion) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
Add a complete todo app example demonstrating progressive enhancement - apps that work both with and without JavaScript enabled.
Features Demonstrated
data-keyattribute neededFiles Added
main.goprogressive-enhancement.tmplprogressive_enhancement_test.goREADME.mdTest plan
go test -v ./progressive-enhancement/...- all E2E tests passDepends on: livetemplate/livetemplate#102
🤖 Generated with Claude Code